iT邦幫忙

2026 iThome 鐵人賽

DAY 27
0
JavaScript

JavaScript為什麼筆記本系列 第 27 篇

Day 27|同一個 function,為什麼換個呼叫方式,this 就變了?

  • 分享至 

  • xImage
  •  

Day 26 在追 Closure 時,有一件事情其實很好理解:

function outer() {
  const name = 'Leo'

  return function inner() {
    console.log(name)
  }
}

inner 要找 name,可以往它建立時的外層 Scope 找。

所以我很自然地開始想:

那 this 呢?

是不是也可以看函式「寫在哪裡」,就知道 this 是誰?

結果不是。

而且我以前一直把 this 理解成:

this 就是「這個物件」。

這個說法有時候看起來會對,然後在下一段程式碼突然把人騙死 XD

今天只追一件事:

一般 function 裡的 this,到底是怎麼決定的?

先從一個完全合理的例子開始

const user = {
  name: 'Leo',

  sayName() {
    console.log(this.name)
  }
}

user.sayName()

結果:

Leo

看起來很好懂。

sayName 在 user 裡面。

所以:

this === user

然後:

this.name

就是:

user.name

如果只看到這裡,我大概會得到一個結論:

function 寫在誰裡面,this 就是誰。

但現在做一個很小的修改。

Function 完全沒改,只是拿出來而已

const user = {
  name: 'Leo',

  sayName() {
    console.log(this.name)
  }
}

const speak = user.sayName

speak()

speak 裡面裝的是什麼?

就是原本那個 function。

我們沒有重新寫一個。

也沒有修改它。

console.log(speak === user.sayName)

結果:

true

明明是同一個 function。

那:

speak()

還會印出 Leo 嗎?

在現在常見的 ES Module/strict mode 環境裡,這裡的 this 會是 undefined。

所以執行:

this.name

甚至可能直接得到錯誤:

TypeError

等一下。

function 明明一模一樣。

到底是哪裡變了?

真正變的是「怎麼呼叫它」

比較這兩行:

user.sayName()

和:

speak()

function 本身沒有改。

改的是 call site,也就是呼叫方式。

第一種:

user.sayName()

可以先很粗略地讀成:

user
  ↓
呼叫 sayName()

這次呼叫裡:

this === user

但第二種:

speak()

前面已經沒有:

user.

了。

它現在是一個普通的 function call。

所以不能因為這個 function「以前住在 user 裡」,就認定 this 永遠是 user。

這跟 Closure 的思路真的不一樣。

Scope 看「函式建立在哪裡」

Day 24 到 Day 26 一直在處理這件事:

const name = 'outside'

function test() {
  console.log(name)
}

name 怎麼找?

這會跟 Lexical Scope 有關。

也就是 function 寫在哪裡、建立在哪個詞法環境裡。

可以粗略想成:

function 建立的位置
        ↓
Scope Chain
        ↓
找外層變數

可是一般 function 的 this,不能用同一套規則直接推。

它更在意:

這一次 function 是怎麼被呼叫的?
```

這是我覺得最容易把 `this` 跟 Closure 混在一起的地方。

兩個東西都出現在 function 裡。

但判斷方法完全不同。

## 同一個 function 可以有不同的 this

再做一個更明顯的實驗。

~~~js
function introduce() {
  console.log(`我是 ${this.name}`)
}

const userA = {
  name: '小明',
  introduce
}

const userB = {
  name: '小華',
  introduce
}

注意:

userA.introduce === userB.introduce

結果是:

true

它們用的是同一個 function。

現在:

userA.introduce()

得到:

我是 小明

然後:

userB.introduce()

得到:

我是 小華

所以如果問:

introduce 的 this 是誰?

其實這個問題少了一個很重要的條件。

比較精確的問法應該是:

這一次呼叫 introduce() 時,this 是誰?

第一次:

userA.introduce()

所以這次是:

this === userA

第二次:

userB.introduce()

所以這次變成:

this === userB

同一段 function 程式碼,可以得到不同的 this。

這件事情如果沒先弄懂,後面看到 callback 很容易開始懷疑人生。

再來一個很常發生的情況

假設有一個物件:

const user = {
  name: 'Leo',

  sayName() {
    console.log(this.name)
  }
}

直接呼叫:

user.sayName()

沒問題。

但如果把它當成 callback 傳出去:

function run(callback) {
  callback()
}

run(user.sayName)

注意 run 收到的是:

user.sayName

也就是 function 這個值。

到了 run 裡面真正執行時:

callback()

呼叫方式已經不是:

user.sayName()

而是:

callback()

這時原本的 user. 已經不在 call site 上了。

所以「把 method 傳出去」這件事,不會自動把原本的 this 一起永久黏著帶走。

這裡跟 Closure 又形成很有趣的對比。

Closure 可以讓 function 持續存取它建立時的外層變數。

但:

const callback = user.sayName

不代表 callback 永遠保有:

this === user

那 Arrow Function 呢?

然後 JavaScript 又跑出另一種 function:

const sayName = () => {
  console.log(this)
}

Arrow Function 的 this 又不走剛才那套。

它沒有自己的 this binding。

它會使用外層詞法環境中的 this。

這也是為什麼下面這種寫法很容易跟想像不同:

const user = {
  name: 'Leo',

  sayName: () => {
    console.log(this.name)
  }
}

user.sayName()

很多人第一眼可能會想:

this === user

但 Arrow Function 不會因為你寫成:

user.sayName()

就得到自己的 this = user。

它的 this 是從外層取得的。

所以這種情況不能拿一般 method 的規則直接套。

這也讓我終於比較能理解,為什麼 JavaScript 同時存在:

function () {}

和:

() => {}

它們並不只是「一個比較舊、一個比較短」。

行為真的有差。

不要看到物件就把 this 翻成「這個物件」

以前看到:

this.name

我會下意識把它翻譯成:

這個物件的 name。

現在我會先停一下。

如果是一般 function,我先找:

這個 function 是怎麼被呼叫的?
```

例如:

~~~js
user.sayName()

跟:

const fn = user.sayName
fn()

即使 fn 和 user.sayName 是同一個 function:

fn === user.sayName

也不代表兩次呼叫的 this 一樣。

所以 this 不是物件的永久身分證。

它比較像是這一次 function 執行時得到的一個呼叫背景。

Closure 和 this 放在一起比較

現在終於可以把 Day 26 和 Day 27 接起來。

const name = 'outside'

const user = {
  name: 'Leo',

  sayName() {
    console.log(name)
    console.log(this.name)
  }
}

裡面有兩個 name。

第一個:

name

會走 Scope Chain。

看的是:

sayName 建立在哪個 Lexical Environment?

第二個:

this.name

一般 function 則要先問:

這一次 sayName 是怎麼被呼叫的?

所以:

name
↓
Lexical Scope
↓
函式寫在哪裡


this
↓
Call Site
↓
函式怎麼被呼叫

這兩條線終於分開了。

先猜一次

function showName() {
  console.log(this.name)
}

const a = {
  name: 'A',
  showName
}

const b = {
  name: 'B',
  showName
}

const fn = a.showName

a.showName()
b.showName()
fn()

先不要跑。

前兩個應該不難。

第三個才是今天真正要看的地方。

前兩次:

A
B

因為呼叫方式分別是:

a.showName()
b.showName()

第三次:

fn()

已經失去 a. 這個呼叫形式。

在 ES Module/strict mode 環境下,this 是 undefined,所以存取:

this.name

會拋出 TypeError。

如果今天只記得一件事

一般 function 的 this 不能只看它寫在哪裡,而要看這一次它是怎麼被呼叫的。

Scope Chain 比較像在問:

我出生在哪裡?

this 比較像在問:

這次是誰用什麼方式叫我?

這也解釋了為什麼同一個 function,可以在不同呼叫中得到不同的 this。

而 Arrow Function 又是另一回事:它沒有自己的 this,而是沿用外層的 this。

下一個「為什麼」

今天第一次看到一件很重要的事情:

run(user.sayName)

我們可以把一個 function 交給另一個 function,然後由它「之後再呼叫」。

可是這又冒出另一個問題。

如果寫:

setTimeout(() => {
  console.log('hello')
}, 1000)

JavaScript 明明一次只能執行一件事情。

那這個 function 到底去了哪裡?

一秒到了,是誰把它叫回來?

如果 JavaScript 是單執行緒,setTimeout 為什麼不會卡在那邊等一秒?

下一篇開始往非同步走。


上一篇
Day 26|外面的 function 都跑完了,裡面的 count 為什麼還記得?
下一篇
Day 28|setTimeout(fn, 0) 都寫 0 了,為什麼還是晚點才執行?
系列文
JavaScript為什麼筆記本 共 28 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言